/*
 *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.appspot.apprtc;

import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.List;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnection;
import org.webrtc.SessionDescription;

/**
 * AsyncTask that converts an AppRTC room URL into the set of signaling
 * parameters to use with that room.
 */
public class RoomParametersFetcher {
	private static final String TAG = "RoomRTCClient";
	private static final int TURN_HTTP_TIMEOUT_MS = 5000;
	private final RoomParametersFetcherEvents events;
	private final String roomUrl;
	private final String roomMessage;

	/**
	 * Room parameters fetcher callbacks.
	 */
	public interface RoomParametersFetcherEvents {
		/**
		 * Callback fired once the room's signaling parameters
		 * SignalingParameters are extracted.
		 */
		void onSignalingParametersReady(final SignalingParameters params);

		/**
		 * Callback for room parameters extraction error.
		 */
		void onSignalingParametersError(final String description);
	}

	public RoomParametersFetcher(
			String roomUrl, String roomMessage, final RoomParametersFetcherEvents events) {
		this.roomUrl = roomUrl;
		this.roomMessage = roomMessage;
		this.events = events;
	}

	public void makeRequest() {
		Log.d(TAG, "Connecting to room: " + roomUrl);
		AsyncHttpURLConnection httpConnection =
				new AsyncHttpURLConnection("POST", roomUrl, roomMessage, new AsyncHttpEvents() {
					@Override
					public void onHttpError(String errorMessage) {
						Log.e(TAG, "Room connection error: " + errorMessage);
						events.onSignalingParametersError(errorMessage);
					}

					@Override
					public void onHttpComplete(String response) {
						roomHttpResponseParse(response);
					}
				});
		httpConnection.send();
	}

	private void roomHttpResponseParse(String response) {
		Log.d(TAG, "Room response: " + response);
		try {
			List<IceCandidate> iceCandidates = null;
			SessionDescription offerSdp = null;
			JSONObject roomJson = new JSONObject(response);

			String result = roomJson.getString("result");
			if (!result.equals("SUCCESS")) {
				events.onSignalingParametersError("Room response error: " + result);
				return;
			}
			response = roomJson.getString("params");
			roomJson = new JSONObject(response);
			String roomId = roomJson.getString("room_id");
			String clientId = roomJson.getString("client_id");
			String wssUrl = roomJson.getString("wss_url");
			String wssPostUrl = roomJson.getString("wss_post_url");
			boolean initiator = (roomJson.getBoolean("is_initiator"));
			if (!initiator) {
				iceCandidates = new ArrayList<>();
				String messagesString = roomJson.getString("messages");
				JSONArray messages = new JSONArray(messagesString);
				for (int i = 0; i < messages.length(); ++i) {
					String messageString = messages.getString(i);
					JSONObject message = new JSONObject(messageString);
					String messageType = message.getString("type");
					Log.d(TAG, "GAE->C #" + i + " : " + messageString);
					if (messageType.equals("offer")) {
						offerSdp = new SessionDescription(
								SessionDescription.Type.fromCanonicalForm(messageType), message.getString("sdp"));
					} else if (messageType.equals("candidate")) {
						IceCandidate candidate = new IceCandidate(
								message.getString("id"), message.getInt("label"), message.getString("candidate"));
						iceCandidates.add(candidate);
					} else {
						Log.e(TAG, "Unknown message: " + messageString);
					}
				}
			}
			Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
			Log.d(TAG, "Initiator: " + initiator);
			Log.d(TAG, "WSS url: " + wssUrl);
			Log.d(TAG, "WSS POST url: " + wssPostUrl);

			List<PeerConnection.IceServer> iceServers =
					iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
			boolean isTurnPresent = false;
			for (PeerConnection.IceServer server : iceServers) {
				Log.d(TAG, "IceServer: " + server);
				for (String uri : server.urls) {
					if (uri.startsWith("turn:")) {
						isTurnPresent = true;
						break;
					}
				}
			}
			// Request TURN servers.
			if (!isTurnPresent && !roomJson.optString("ice_server_url").isEmpty()) {
				List<PeerConnection.IceServer> turnServers =
						requestTurnServers(roomJson.getString("ice_server_url"));
				for (PeerConnection.IceServer turnServer : turnServers) {
					Log.d(TAG, "TurnServer: " + turnServer);
					iceServers.add(turnServer);
				}
			}

			SignalingParameters params = new SignalingParameters(
					iceServers, initiator, clientId, wssUrl, wssPostUrl, offerSdp, iceCandidates);
			events.onSignalingParametersReady(params);
		} catch (JSONException e) {
			events.onSignalingParametersError("Room JSON parsing error: " + e.toString());
		} catch (IOException e) {
			events.onSignalingParametersError("Room IO error: " + e.toString());
		}
	}

	// Requests & returns a TURN ICE Server based on a request URL.  Must be run
	// off the main thread!
	private List<PeerConnection.IceServer> requestTurnServers(String url)
			throws IOException, JSONException {
		List<PeerConnection.IceServer> turnServers = new ArrayList<>();
		Log.d(TAG, "Request TURN from: " + url);
		HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
		connection.setDoOutput(true);
		connection.setRequestProperty("REFERER", "https://appr.tc");
		connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS);
		connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS);
		int responseCode = connection.getResponseCode();
		if (responseCode != 200) {
			throw new IOException("Non-200 response when requesting TURN server from " + url + " : "
					+ connection.getHeaderField(null));
		}
		InputStream responseStream = connection.getInputStream();
		String response = drainStream(responseStream);
		connection.disconnect();
		Log.d(TAG, "TURN response: " + response);
		JSONObject responseJSON = new JSONObject(response);
		JSONArray iceServers = responseJSON.getJSONArray("iceServers");
		for (int i = 0; i < iceServers.length(); ++i) {
			JSONObject server = iceServers.getJSONObject(i);
			JSONArray turnUrls = server.getJSONArray("urls");
			String username = server.has("username") ? server.getString("username") : "";
			String credential = server.has("credential") ? server.getString("credential") : "";
			for (int j = 0; j < turnUrls.length(); j++) {
				String turnUrl = turnUrls.getString(j);
				PeerConnection.IceServer turnServer =
						PeerConnection.IceServer.builder(turnUrl)
								.setUsername(username)
								.setPassword(credential)
								.createIceServer();
				turnServers.add(turnServer);
			}
		}
		return turnServers;
	}

	// Return the list of ICE servers described by a WebRTCPeerConnection
	// configuration string.
	private List<PeerConnection.IceServer> iceServersFromPCConfigJSON(String pcConfig)
			throws JSONException {
		JSONObject json = new JSONObject(pcConfig);
		JSONArray servers = json.getJSONArray("iceServers");
		List<PeerConnection.IceServer> ret = new ArrayList<>();
		for (int i = 0; i < servers.length(); ++i) {
			JSONObject server = servers.getJSONObject(i);
			String url = server.getString("urls");
			String credential = server.has("credential") ? server.getString("credential") : "";
			PeerConnection.IceServer turnServer =
					PeerConnection.IceServer.builder(url)
							.setPassword(credential)
							.createIceServer();
			ret.add(turnServer);
		}
		return ret;
	}

	// Return the contents of an InputStream as a String.
	private static String drainStream(InputStream in) {
		Scanner s = new Scanner(in, "UTF-8").useDelimiter("\\A");
		return s.hasNext() ? s.next() : "";
	}
}
